/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hive.ql.processors; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Map; import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.compress.archivers.jar.JarArchiveEntry; import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream; import org.apache.commons.io.IOUtils; import org.apache.hadoop.hive.conf.HiveVariableSource; import org.apache.hadoop.hive.conf.VariableSubstitution; import org.apache.hadoop.hive.ql.CommandNeedRetryException; import org.apache.hadoop.hive.ql.metadata.HiveException; import org.apache.hadoop.hive.ql.security.authorization.plugin.HiveOperationType; import org.apache.hadoop.hive.ql.session.SessionState; import org.apache.hadoop.hive.ql.session.SessionState.LogHelper; import org.apache.hadoop.hive.ql.session.SessionState.ResourceType; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.types.Path; import org.codehaus.groovy.ant.Groovyc; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.Files; /** * Processor allows users to build code inside a hive session, then * use this code as a UDF, Serde, or even a more complex entity like an * input format or hook. * * Note: This class is stateful and not thread safe. Create a new instance for * each invocation of CompileProcessor. * */ public class CompileProcessor implements CommandProcessor { public static final Logger LOG = LoggerFactory.getLogger(CompileProcessor.class.getName()); public static final LogHelper console = new LogHelper(LOG); public static final String IO_TMP_DIR = "java.io.tmpdir"; public static final String GROOVY = "GROOVY"; public static final String AS = "AS"; public static final String NAMED = "NAMED"; private static final String SYNTAX = "syntax: COMPILE ` some code here ` AS groovy NAMED something.groovy"; private static final AtomicInteger runCount; /** * The language of the compiled code. Used to select the appropriate compiler. */ private String lang; /** * The code to be compiled */ private String code; /** * The name of the file the code will be written to */ private String named; /** * The entire command sent to the processor */ private String command; /** * Used as part of a file name to help avoid collisions. */ private int myId; static { runCount = new AtomicInteger(0); } @Override public void init() { //no init needed } /** * User supplies dynamic code in this format: * COMPILE ` some code here ` AS groovy NAMED something.groovy; * CompileProcessor will compile and package this code into a jar. The jar * will be added to the session state via the session state's * ADD RESOURCE command. * @param command a String to be compiled * @return CommandProcessorResponse with 0 for success and 1 for failure */ @Override public CommandProcessorResponse run(String command) throws CommandNeedRetryException { SessionState ss = SessionState.get(); this.command = command; CommandProcessorResponse authErrResp = CommandUtil.authorizeCommand(ss, HiveOperationType.COMPILE, Arrays.asList(command)); if(authErrResp != null){ // there was an authorization issue return authErrResp; } myId = runCount.getAndIncrement(); try { parse(ss); } catch (CompileProcessorException e) { return CommandProcessorResponse.create(e); } CommandProcessorResponse result = null; try { result = compile(ss); } catch (CompileProcessorException e) { return CommandProcessorResponse.create(e); } return result; } /** * Parses the supplied command * @param ss * @throws CompileProcessorException if the code can not be compiled or the jar can not be made */ @VisibleForTesting void parse(SessionState ss) throws CompileProcessorException { if (ss != null){ command = new VariableSubstitution(new HiveVariableSource() { @Override public Map<String, String> getHiveVariable() { return SessionState.get().getHiveVariables(); } }).substitute(ss.getConf(), command); } if (command == null || command.length() == 0) { throw new CompileProcessorException("Command was empty"); } StringBuilder toCompile = new StringBuilder(); int startPosition = 0; int endPosition = -1; /* TODO Escape handling may be changed by a follow on. * The largest issue is ; which are treated as statement * terminators for the cli. Once the cli is fixed this * code should be re-investigated */ while (command.charAt(startPosition++) != '`' && startPosition < command.length()){ } if (startPosition == command.length()){ throw new CompileProcessorException(SYNTAX); } for (int i = startPosition; i < command.length(); i++) { if (command.charAt(i) == '\\') { toCompile.append(command.charAt(i + 1)); i = i + 1; continue; } else if (command.charAt(i) == '`'){ endPosition = i; break; } else { toCompile.append(command.charAt(i)); } } if (endPosition == -1){ throw new CompileProcessorException(SYNTAX); } StringTokenizer st = new StringTokenizer(command.substring(endPosition+1), " "); if (st.countTokens() != 4){ throw new CompileProcessorException(SYNTAX); } String shouldBeAs = st.nextToken(); if (!shouldBeAs.equalsIgnoreCase(AS)){ throw new CompileProcessorException(SYNTAX); } setLang(st.nextToken()); if (!lang.equalsIgnoreCase(GROOVY)){ throw new CompileProcessorException("Can not compile " + lang + ". Hive can only compile " + GROOVY); } String shouldBeNamed = st.nextToken(); if (!shouldBeNamed.equalsIgnoreCase(NAMED)){ throw new CompileProcessorException(SYNTAX); } setNamed(st.nextToken()); setCode(toCompile.toString()); } @VisibleForTesting /** * Method converts statement into a file, compiles the file and then packages the file. * @param ss * @return Response code of 0 for success 1 for failure * @throws CompileProcessorException */ CommandProcessorResponse compile(SessionState ss) throws CompileProcessorException { Project proj = new Project(); String ioTempDir = System.getProperty(IO_TMP_DIR); File ioTempFile = new File(ioTempDir); if (!ioTempFile.exists()){ throw new CompileProcessorException(ioTempDir + " does not exists"); } if (!ioTempFile.isDirectory() || !ioTempFile.canWrite()){ throw new CompileProcessorException(ioTempDir + " is not a writable directory"); } Groovyc g = new Groovyc(); long runStamp = System.currentTimeMillis(); String jarId = myId + "_" + runStamp; g.setProject(proj); Path sourcePath = new Path(proj); File destination = new File(ioTempFile, jarId + "out"); g.setDestdir(destination); File input = new File(ioTempFile, jarId + "in"); sourcePath.setLocation(input); g.setSrcdir(sourcePath); input.mkdir(); File fileToWrite = new File(input, this.named); try { Files.write(this.code, fileToWrite, Charset.forName("UTF-8")); } catch (IOException e1) { throw new CompileProcessorException("writing file", e1); } destination.mkdir(); try { g.execute(); } catch (BuildException ex){ throw new CompileProcessorException("Problem compiling", ex); } File testArchive = new File(ioTempFile, jarId + ".jar"); JarArchiveOutputStream out = null; try { out = new JarArchiveOutputStream(new FileOutputStream(testArchive)); for (File f: destination.listFiles()){ JarArchiveEntry jentry = new JarArchiveEntry(f.getName()); FileInputStream fis = new FileInputStream(f); out.putArchiveEntry(jentry); IOUtils.copy(fis, out); fis.close(); out.closeArchiveEntry(); } out.finish(); } catch (IOException e) { throw new CompileProcessorException("Exception while writing jar", e); } finally { if (out!=null){ try { out.close(); } catch (IOException WhatCanYouDo) { } } } if (ss != null){ ss.add_resource(ResourceType.JAR, testArchive.getAbsolutePath()); } CommandProcessorResponse good = new CommandProcessorResponse(0, testArchive.getAbsolutePath(), null); return good; } public String getLang() { return lang; } public void setLang(String lang) { this.lang = lang; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getNamed() { return named; } public void setNamed(String named) { this.named = named; } public String getCommand() { return command; } class CompileProcessorException extends HiveException { private static final long serialVersionUID = 1L; CompileProcessorException(String s, Throwable t) { super(s, t); } CompileProcessorException(String s) { super(s); } } }